﻿// -----------------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
// -----------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel.Composition.Primitives;
using System.Linq;
using Microsoft.Internal;
using Microsoft.Internal.Collections;

namespace System.ComponentModel.Composition.Hosting
{
    public class AggregateExportProvider : ExportProvider , IDisposable
    {
        private Lock _lock = new Lock();
        private ReadOnlyCollection<ExportProvider> _providers;
        private volatile bool _isDisposed = false;
        private EventHandler<ExportsChangedEventArgs> _changedEventStorage;

        /// <summary>
        /// Initializes a new instance of the <see cref="AggregateExportProvider"/> class.
        /// </summary>
        /// <param name="providers">The prioritized list of export providers.</param>
        /// <remarks>
        ///     <para>
        ///         The <see cref="AggregateExportProvider"/> will consult the providers in the order they have been specfied when 
        ///         executing <see cref="ExportProvider.GetExports(ImportDefinition)"/>. 
        ///     </para>
        ///     <para>
        ///         The <see cref="AggregateExportProvider"/> does not take ownership of the specified providers. 
        ///         That is, it will not try to dispose of any of them when it gets disposed.
        ///     </para>
        /// </remarks>
        public AggregateExportProvider(params ExportProvider[] providers) : 
            this(providers.AsEnumerable())
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="AggregateExportProvider"/> class.
        /// </summary>
        /// <param name="providers">The prioritized list of export providers. The providers are consulted in order in which they are supplied.</param>
        /// <remarks>
        ///     <para>
        ///         The <see cref="AggregateExportProvider"/> will consult the providers in the order they have been specfied when 
        ///         executing <see cref="ExportProvider.GetExports(ImportDefinition)"/>. 
        ///     </para>
        ///     <para>
        ///         The <see cref="AggregateExportProvider"/> does not take ownership of the specified providers. 
        ///         That is, it will not try to dispose of any of them when it gets disposed.
        ///     </para>
        /// </remarks>
        public AggregateExportProvider(IEnumerable<ExportProvider> providers)
        {
            Requires.NullOrNotNullElements(providers, "providers");

            // this will always fully copy the array
            this._providers = new ReadOnlyCollection<ExportProvider>(providers == null ? new ExportProvider[0] : providers.ToArray());

            // we are in the constructor, so there's no need to lock anything
            this._providers.ForEach(provider =>
            {
                provider.ExportsChanged += this.InternalOnExportChanged;
            });
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (!this._isDisposed)
                {
                    bool disposeLock = false;
                    IEnumerable<ExportProvider> providers = null;
                    try
                    {
                        using (new WriteLock(this._lock))
                        {
                            if (!this._isDisposed)
                            {
                                providers = this._providers;
                                this._providers = null;
                                disposeLock = true;
                                this._isDisposed = true;
                            }
                        }
                    }
                    finally
                    {
                        if (disposeLock)
                        {
                            this._lock.Dispose();
                            this._lock = null;
                        }

                        if (providers != null)
                        {
                            providers.ForEach(provider =>
                            {
                                provider.ExportsChanged -= this.InternalOnExportChanged;
                            });
                        }
                    }
                }
            }
        }

        /// <summary>
        ///     Gets the export providers which the aggregate export provider aggregates.
        /// </summary>
        /// <value>
        ///     A <see cref="ReadOnlyCollection{T}"/> of <see cref="ExportProvider"/> objects
        ///     which the <see cref="AggregateExportProvider"/> aggregates.
        /// </value>
        /// <exception cref="ObjectDisposedException">
        ///     The <see cref="AggregateExportProvider"/> has been disposed of.
        /// </exception>
        public ReadOnlyCollection<ExportProvider> Providers
        {
            get
            {
                this.ThrowIfDisposed();

                return this._providers;
            }
        }

        /// <summary>
        ///     Occurs when the exports in the <see cref="ExportProvider"/> change.
        /// </summary>
        /// <remarks>
        ///     The <see cref="ExportsChanged" /> event passes an <see cref="ExportsChangedEventArgs" />
        ///     which provides a list of contract names for the exports that have changed. An export
        ///     is considered to have changed if it has been added, removed, or updated in the 
        ///     <see cref="AggregateExportProvider" />.
        /// </remarks>
        /// <exception cref="ObjectDisposedException">
        ///     The <see cref="AggregateExportProvider"/> has been disposed of.
        /// </exception>
        public override event EventHandler<ExportsChangedEventArgs> ExportsChanged
        {
            add
            {
                this.ThrowIfDisposed();

                using (new WriteLock(this._lock))
                {
                    this._changedEventStorage += value;
                }
            }
            remove
            {
                this.ThrowIfDisposed();

                using (new WriteLock(this._lock))
                {
                    this._changedEventStorage -= value;
                }
            }
        }

        private void InternalOnExportChanged(object sender, ExportsChangedEventArgs e)
        {
            this.OnExportsChanged(e);
        }

        /// <summary>
        /// Raises the <see cref="AggregateExportProvider.ExportsChanged" /> event.
        /// </summary>
        /// <param name="e">The <see cref="System.ComponentModel.Composition.Hosting.ExportsChangedEventArgs"/> instance containing the event data.</param>
        protected override void OnExportsChanged(ExportsChangedEventArgs e)
        {
            EventHandler<ExportsChangedEventArgs> changedEvent = null;

            using (new ReadLock(this._lock))
            {
                changedEvent = this._changedEventStorage;
            }

            if (changedEvent != null)
            {
                CompositionResult result = CompositionServices.TryFire(changedEvent, this, e);
                result.ThrowOnErrors();
            }
        }

        /// <summary>
        /// Returns all exports that match the conditions of the specified import.
        /// </summary>
        /// <param name="definition">The <see cref="ImportDefinition"/> that defines the conditions of the
        /// <see cref="Export"/> to get.</param>
        /// <returns></returns>
        /// <result>
        /// An <see cref="IEnumerable{T}"/> of <see cref="Export"/> objects that match
        /// the conditions defined by <see cref="ImportDefinition"/>, if found; otherwise, an
        /// empty <see cref="IEnumerable{T}"/>.
        /// </result>
        /// <remarks>
        /// 	<note type="inheritinfo">
        /// The implementers should not treat the cardinality-related mismatches as errors, and are not
        /// expected to throw exceptions in those cases.
        /// For instance, if the import requests exactly one export and the provider has no matching exports or more than one,
        /// it should return an empty <see cref="IEnumerable{T}"/> of <see cref="Export"/>.
        /// </note>
        /// </remarks>
        protected override IEnumerable<Export> GetExportsCore(ImportDefinition definition)
        {
            this.ThrowIfDisposed();

            if (definition.Cardinality == ImportCardinality.ZeroOrMore)
            {
                // if asked for "many" we simply merge all values
                // NOTE : we merge all the values together, because the reprecussions of not doing so are severe - 
                // NOTE : we use our very own ConcatMany - as opposed to SelectAny - because the latter will evaluate all GetExports()
                // upfront as opposed to the former that will keep calling them on every iteration over the result.

                // TODO : we might consider doing just "ToArray" here - this will materialize all exports, and in some scenarios it 
                // seems faster
                return this._providers.ConcatMany(provider => provider.GetExports(definition));
            }
            else
            {
                IEnumerable<Export> allExports = Enumerable.Empty<Export>();

                // if asked for "one or less", the prioriry is at play - the first provider that agrees to return the value 
                // which best complies with the request, wins.
                foreach (ExportProvider provider in this._providers)
                {
                    IEnumerable<Export> exports;
                    bool cardinalityCheckResult = provider.TryGetExports(definition, out exports);
                    if (cardinalityCheckResult && exports.Any())
                    {
                        // NOTE : if the provider returned nothing, we need to proceed, even if it indicated that the
                        // cardinality is correct - when asked for "one or less", the provider might - correctly - 
                        // return an empty sequence, but we shouldn't be satisfied with that as providers down the list
                        // might have a value we are interested in.
                        return exports;
                    }
                    else
                    {
                        // TODO
                        // This is a sneaky thing that we do - if in the end no provider returns the exports with the right cardinality
                        // we simply return the aggregation of all exports they have restuned. This way the end result is still not waht we want
                        // but no information is lost.
                        // WE SHOULD fix this behavior, but this is ONLY possible if we can treat many exports as no exports for the sake of singles
                        if (exports.Any())
                        {
                            allExports = allExports.Concat(exports);
                        }
                    }
                }

                return allExports;
            }
        }

        private void ThrowIfDisposed()
        {
            if (this._isDisposed)
            {
                throw ExceptionBuilder.CreateObjectDisposed(this);
            }
        }
    }
}
